package com.element.oauth2.server.global.converter;

import com.element.oauth2.constant.SecurityParams;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.util.*;

public final class OAuthAuthorizationCodeRequestConverter implements AuthenticationConverter {

    private final Authentication ANONYMOUS_AUTHENTICATION = new AnonymousAuthenticationToken("anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
    private final RequestMatcher OIDC_REQUEST_MATCHER = createOidcRequestMatcher();

    public OAuthAuthorizationCodeRequestConverter() {
    }

    public Authentication convert(HttpServletRequest request) {
        if (!"GET".equals(request.getMethod()) && !OIDC_REQUEST_MATCHER.matches(request)) {
            return null;
        } else {
            MultiValueMap<String, String> parameters = getParameters(request);
            String responseType = request.getParameter(SecurityParams.RESPONSE_TYPE);
            if (StringUtils.hasText(responseType) && (parameters.get(SecurityParams.RESPONSE_TYPE)).size() == 1) {
                if (!responseType.equals(OAuth2AuthorizationResponseType.CODE.getValue())) {
                    throwError("unsupported_response_type", SecurityParams.RESPONSE_TYPE);
                }
            } else {
                throwError("invalid_request", SecurityParams.RESPONSE_TYPE);
            }

            String authorizationUri = request.getRequestURL().toString();
            String clientId = parameters.getFirst(SecurityParams.CLIENT_ID);
            if (!StringUtils.hasText(clientId) || (parameters.get(SecurityParams.CLIENT_ID)).size() != 1) {
                throwError("invalid_request", SecurityParams.CLIENT_ID);
            }

            Authentication principal = SecurityContextHolder.getContext().getAuthentication();
            if (principal == null) {
                principal = ANONYMOUS_AUTHENTICATION;
            }

            String redirectUri = parameters.getFirst(SecurityParams.REDIRECT_URL);
            if (StringUtils.hasText(redirectUri) && (parameters.get(SecurityParams.REDIRECT_URL)).size() != 1) {
                throwError("invalid_request", SecurityParams.REDIRECT_URL);
            }

            Set<String> scopes = null;
            String scope = parameters.getFirst(SecurityParams.SCOPE);
            if (StringUtils.hasText(scope) && (parameters.get(SecurityParams.SCOPE)).size() != 1) {
                throwError("invalid_request", SecurityParams.SCOPE);
            }

            if (StringUtils.hasText(scope)) {
                scopes = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
            }

            String state = parameters.getFirst(SecurityParams.STATE);
            if (StringUtils.hasText(state) && (parameters.get(SecurityParams.STATE)).size() != 1) {
                throwError("invalid_request", SecurityParams.STATE);
            }

            String codeChallenge = parameters.getFirst("code_challenge");
            if (StringUtils.hasText(codeChallenge) && (parameters.get("code_challenge")).size() != 1) {
                throwError("invalid_request", "code_challenge");
            }

            String codeChallengeMethod = parameters.getFirst("code_challenge_method");
            if (StringUtils.hasText(codeChallengeMethod) && (parameters.get("code_challenge_method")).size() != 1) {
                throwError("invalid_request", "code_challenge_method");
            }

            Map<String, Object> additionalParameters = new HashMap<>();
            parameters.forEach((key, value) -> {
                if (!key.equals(SecurityParams.RESPONSE_TYPE)
                        && !key.equals(SecurityParams.CLIENT_ID)
                        && !key.equals(SecurityParams.REDIRECT_URL)
                        && !key.equals(SecurityParams.SCOPE)
                        && !key.equals(SecurityParams.STATE)) {
                    additionalParameters.put(key, value.size() == 1 ? value.get(0) : value.toArray(new String[0]));
                }

            });
            return new OAuth2AuthorizationCodeRequestAuthenticationToken(authorizationUri, clientId, principal, redirectUri, state, scopes, additionalParameters);
        }
    }

    private MultiValueMap<String, String> getParameters(HttpServletRequest request) {
        Map<String, String[]> parameterMap = request.getParameterMap();
        MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(parameterMap.size());
        parameterMap.forEach((key, values) -> {
            for (String value : values) {
                parameters.add(key, value);
            }
        });
        return parameters;
    }

    private RequestMatcher createOidcRequestMatcher() {
        RequestMatcher postMethodMatcher = (request) -> "POST".equals(request.getMethod());
        RequestMatcher responseTypeParameterMatcher = (request) -> request.getParameter(SecurityParams.RESPONSE_TYPE) != null;
        RequestMatcher openidScopeMatcher = (request) -> {
            String scope = request.getParameter(SecurityParams.SCOPE);
            return StringUtils.hasText(scope) && scope.contains(SecurityParams.OPEN_ID);
        };
        return new AndRequestMatcher(postMethodMatcher, responseTypeParameterMatcher, openidScopeMatcher);
    }

    private void throwError(String errorCode, String parameterName) {
        OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, null);
        throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);
    }
}